package com.espirit.smc.client;

import static com.espirit.util.Constant.FORMATTER;
import static com.espirit.util.Constant.METHOD_COMPARE;
import static com.espirit.util.Constant.METHOD_UPDATE_PKG;
import static com.espirit.util.Constant.METHOD_UPLOAD;

import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.apache.http.Consts;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.type.CollectionType;
import org.codehaus.jackson.map.type.SimpleType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.espirit.file.CompressFileUtil;
import com.espirit.file.FileScanTask;
import com.espirit.file.NFile;
import com.espirit.file.OperateKind;
import com.espirit.http.HttpClientManager;
import com.espirit.smc.CompareResult;
import com.espirit.smc.server.Server;
import com.espirit.util.CommonUtils;

public class Client {

	private static final Logger logger = LoggerFactory.getLogger(Client.class);

	private ClientProperties properties;

	public Client(ClientProperties properties){
		this.properties = properties;
	}

	public Path makeFileList() throws IOException, InterruptedException, ExecutionException, TimeoutException {
		Set<Path> paths = properties.getScanPaths();
		Set<Path> ignorePaths = properties.getIgnoreSubDirPaths();
		List<Future<Set<NFile>>> results = new LinkedList<Future<Set<NFile>>>();
		ExecutorService es = Executors.newCachedThreadPool();
		for (Path path : paths) {
			if (Files.exists(path) && Files.isDirectory(path)) {
				if (ignorePaths!=null && ignorePaths.contains(path)) {
					results.add(es.submit(new FileScanTask(path,true)));
				}else{
					results.add(es.submit(new FileScanTask(path)));
				}
			}
		}
		Path tempPath = properties.getTempPath();
		if (!Files.exists(tempPath)) {
			Files.createDirectory(tempPath);
		}
		Path tempFile = Files.createTempFile(tempPath, "filelist", ".lst");
		Path root = properties.getRootPath();
		try (OutputStream os = new FileOutputStream(tempFile.toFile());
			 OutputStreamWriter writer = new OutputStreamWriter(os, Consts.UTF_8);
			 BufferedWriter bw = new BufferedWriter(writer)) {
			for (Future<Set<NFile>> result : results) {
				Set<NFile> nfiles = result.get(2l, TimeUnit.MINUTES);
				for (NFile file : nfiles) {
					Path filePath = file.getRelativePath(root);
					String lastUpdateTime = FORMATTER.format(file.getLastModifiedTime().toInstant());
					bw.write(filePath.toString() + "\t" + lastUpdateTime);
					bw.newLine();
				}
				bw.flush();
			}

		}
		es.shutdown();
		return tempFile.toAbsolutePath();
	}

	public String uploadFileList(String filePath) {
		FileSystem fs = FileSystems.getDefault();
		return uploadFileList(fs.getPath(filePath));
	}

	public String uploadFileList(Path filePath) {
		HttpPost post;
		HttpClient client;
		HttpResponse response = null;
		MultipartEntityBuilder builder;
		String result = null;
		String url = properties.getServerURL();
		try {
			File file = filePath.toFile();
			FileBody fileBody = new FileBody(file);
			builder = MultipartEntityBuilder.create();
			builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
			builder.addPart("file", fileBody);
			post = HttpClientManager.getPostMethod(url + "?method=" + METHOD_UPLOAD);

			client = HttpClientBuilder.create().build();
			HttpEntity entity = builder.build();
			post.setEntity(entity);
			response = client.execute(post);
			HttpEntity res = response.getEntity();
			String contentType = res.getContentType().getValue();
			StatusLine statusLine= response.getStatusLine();
			if (statusLine!=null && HttpServletResponse.SC_OK == statusLine.getStatusCode() && !"text/html;charset=UTF-8".equals(contentType)) {
				throw new RuntimeException("load update package failed!");
			}
			result = IOUtils.toString(res.getContent(),Consts.UTF_8);
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			throw new RuntimeException("upload file failed!" + e.getMessage(), e);
		}
		return result;
	}

	public String loadDifferentList(String fileName,boolean isRemote) {
		String result;
		if (isRemote) {
			result = loadDifferentFromRemoteList(fileName);
		}else{
			result = loadDifferentFromLocalList(fileName);
		}
		return result;
	}
	
	private String loadDifferentFromLocalList(String fileName) {
		String result = null;
		FileSystem fs = FileSystems.getDefault();
		Path tempPath = properties.getTempPath();
		Path rootPath = properties.getServerRootPath();
		Set<Path> watchPaths= properties.getWatchPaths();
		Path fileListPath = fs.getPath(tempPath.toString(), fileName);
		try {
			Map<String, Instant> fileList = Server.parseFileList(fileListPath);
			List<CompareResult> crs = Server.loadDiffrenFileList(fileList, rootPath, watchPaths,properties.getServerIgnoreSubDirPaths());
			if (crs != null && !crs.isEmpty()) {
				result = CommonUtils.objectToJSON(crs);
			}else{
				result = "[]";
			}
		} catch (Exception e) {
			throw new RuntimeException("load differents from server failed!" + e.getMessage(), e);
		}
		return result;
	}

	private String loadDifferentFromRemoteList(String fileName) {
		HttpPost post;
		HttpClient client;
		HttpResponse response = null;
		String result = null;
		String url = properties.getServerURL();
		try {
			post = HttpClientManager.getPostMethod(url);
			client = HttpClientBuilder.create().build();
			List<NameValuePair> params = new ArrayList<NameValuePair>();
			params.add(new BasicNameValuePair("method", METHOD_COMPARE));
			params.add(new BasicNameValuePair("fileName", fileName));
			UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, Consts.UTF_8);
			post.setEntity(entity);
			response = client.execute(post);
			HttpEntity res = response.getEntity();
			String contentType = res.getContentType().getValue();
			StatusLine statusLine= response.getStatusLine();
			if (statusLine!=null && HttpServletResponse.SC_OK == statusLine.getStatusCode() && !"text/html;charset=UTF-8".equals(contentType)) {
				throw new RuntimeException("load update package failed!");
			}
			result = IOUtils.toString(res.getContent(),Consts.UTF_8);
			logger.info(result);
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			throw new RuntimeException("load differents from server failed!" + e.getMessage(), e);
		}
		return result;
	}

	public Path loadUpdatePackage(String fileList) {
		HttpPost post;
		HttpClient client;
		HttpResponse response = null;
		Path updatePkgPath = null;
		String url = properties.getServerURL();
		Path tmpPath = properties.getTempPath();
		try {
			post = HttpClientManager.getPostMethod(url);
			client = HttpClientBuilder.create().build();
			List<NameValuePair> params = new ArrayList<NameValuePair>();
			params.add(new BasicNameValuePair("method", METHOD_UPDATE_PKG));
			params.add(new BasicNameValuePair("fileList", fileList));
			UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, Consts.UTF_8);
			post.setEntity(entity);
			response = client.execute(post);
			HttpEntity res = response.getEntity();
			String contentType = res.getContentType().getValue();
			StatusLine statusLine= response.getStatusLine();
			if (statusLine!=null && HttpServletResponse.SC_OK == statusLine.getStatusCode() && !"bin;charset=UTF-8".equals(contentType)) {
				throw new RuntimeException("load update package failed!");
			}
			Header cdHeader = response.getFirstHeader("Content-Disposition");
			if (cdHeader!=null) {
				String cd = cdHeader.getValue();
				String fileName = cd.split(";")[1].split("=")[1];
				updatePkgPath = FileSystems.getDefault().getPath(tmpPath.toAbsolutePath().toString(), fileName);
			}else{
				updatePkgPath = Files.createTempFile(tmpPath, "update", ".zip");
			}
			InputStream is = res.getContent();
			try (OutputStream os = new FileOutputStream(updatePkgPath.toFile()); ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
				IOUtils.copy(is, bos);
				bos.writeTo(os);
				bos.flush();
				os.flush();
			}
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			throw new RuntimeException("load update  file failed!", e);
		}
		return updatePkgPath;
	}

	public void update(Path updatePackage) throws IOException {
		String root = properties.getRoot();
		Path rootPath = properties.getRootPath();
		URI zipUri = updatePackage.toUri();
		FileSystem fs = FileSystems.getDefault();
		try {
			URI uri = new URI("jar:" + zipUri.getScheme(), zipUri.getPath(), null);
			Map<String, String> env = new HashMap<>(1);
			env.put("create", String.valueOf(!Files.exists(updatePackage)));
			env.put("encoding", "UTF-8");
			String fileChangeList = null;
			try (FileSystem zipfs = FileSystems.newFileSystem(uri, env);) {
				byte[] bytes = Files.readAllBytes(zipfs.getPath("filelist.lst"));
				fileChangeList = new String(bytes,Consts.UTF_8);
			}
			CompressFileUtil.uncompress(updatePackage, rootPath);
			if (fileChangeList!=null) {
				ObjectMapper mapper = new ObjectMapper();
				List<CompareResult> list = mapper.readValue(fileChangeList, CollectionType.construct(List.class, SimpleType.construct(CompareResult.class)));
				for (CompareResult result : list) {
					if (OperateKind.DELETE == result.getKind()) {
						String del = result.getPath();
						Path delPath = fs.getPath(root, del);
						Files.deleteIfExists(delPath);
					}
					logger.info(result.toString());
				}
			}
		} catch (URISyntaxException e) {
			logger.error(e.getMessage(), e);
			throw new RuntimeException("update failed!", e);
		}
	}
	
	public void update(String fileList){
		ObjectMapper mapper = new ObjectMapper();
		FileSystem fs = FileSystems.getDefault();
		String root = properties.getRoot();
		String serverRoot = properties.getServerRoot();
		try {
			List<CompareResult> list = mapper.readValue(fileList, CollectionType.construct(List.class, SimpleType.construct(CompareResult.class)));
			for (CompareResult result : list) {
				if (OperateKind.DELETE == result.getKind()) {
					String del = result.getPath();
					Path delPath = fs.getPath(root, del);
					Files.deleteIfExists(delPath);
				}else if(OperateKind.CREATE == result.getKind() || OperateKind.MODIFY == result.getKind()){
					String fp = result.getPath();
					Path source = fs.getPath(serverRoot, fp);
					Path target = fs.getPath(root, fp);
					if (!Files.exists(target.getParent())) {
						Files.createDirectories(target.getParent());
					}
					if (Files.exists(source)) {
						Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
					}
				} 
				logger.info(result.toString());
			}
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			throw new RuntimeException("update failed!", e);
		} 
	}
}
